package org.json.sax;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;

import org.json.sax.JSONParser.JSONParsingException;

public class JSONStreamTokenizer implements JSONTokenizer {

	private static final String UTF_8 = "utf-8";

	private static final int PREV_CHAR_OVERLAP = 1;
	private static final int SNIPPET_LEN = 20; // 

	private static final int DEFAUL_BUFFER_SIZE = 1024; 
	private final int BUFFER_LEN;
	

	private InputStreamReader reader;
	private final char[] buffer;
    private int pos = 0;
    private int limit = 0;
	private int at = 0;
	
	public JSONStreamTokenizer(InputStream inStream) {
		try {
			reader = new InputStreamReader(inStream, UTF_8);
			BUFFER_LEN = DEFAUL_BUFFER_SIZE;
			buffer = new char[BUFFER_LEN + PREV_CHAR_OVERLAP];
		} catch (UnsupportedEncodingException e) {
			throw new IllegalArgumentException(
					"Unsupported char encoding UTF-8: " + e);
		}
	}

	/**
	 * NOTICE: Maybe used just for test purposes
	 **/
	public JSONStreamTokenizer(InputStream inStream, final int bufferSize)
			throws UnsupportedEncodingException {

		reader = new InputStreamReader(inStream, UTF_8);
		BUFFER_LEN = bufferSize;
		buffer = new char[BUFFER_LEN + PREV_CHAR_OVERLAP];
	}

	public int getIndex() {
		return at;
	}

	public boolean more() {
		return pos < limit;
	}

	/**
	 * NOTICE Must be used just 
	 * after nextXX() calls, in cases
	 * when you don't want consume the
	 * the current char
	 */
	public boolean back() {
		if (at == 0)
			return false;
		--at;
		boolean x = pos > 0;
		if (x)
			--pos;
		return x;
	}

	public char next() throws JSONParsingException {
		try {
			if (more() || fillBuffer(1)) {
				++at;
				return buffer[pos++];
			}
			return 0;
		} catch (IOException e) {
			throw new JSONParsingException(e);
		}
	}

	public char nextClean() throws JSONParsingException {
		for (;;) {
			char c = next();
			if (c == '/') {
				switch (next()) {
				case '/':
					do {
						c = next();
					} while (c != '\n' && c != '\r' && c != 0);
					break;
				case '*':
					for (;;) {
						c = next();
						if (c == 0) {
							throw syntaxError("Unclosed comment.");
						}
						if (c == '*') {
							if (next() == '/') {
								break;
							}
							back();
						}
					}
					break;
				default:
					back();
					return '/';
				}
			} else if (c == '#') {
				do {
					c = next();
				} while (c != '\n' && c != '\r' && c != 0);
			} else if (c == 0 || c > ' ') {
				return c;
			}
		}
	}

	public String next(int n) throws JSONParsingException {
        try {
			if (n > BUFFER_LEN || (pos + n > limit && !fillBuffer(n))) {
			   throw syntaxError("Substring bounds error");
			}
			String s = new String(buffer, pos, n);
	        pos += n;
	        at += n;
	        return s;

        } catch (IOException e) {
			throw new JSONParsingException(e);
		}
	}

	/**
     * Returns the string up to but not including {@code quote}, unescaping any
     * character escape sequences encountered along the way. The opening quote
     * should have already been read. This consumes the closing quote, but does
     * not include it in the returned string.
     *
     * @param quote either ' or ".
     * @throws NumberFormatException if any unicode escape sequences are
     *     malformed.
     */
    public String nextString(char quote) throws JSONParsingException {
        try {
        	StringBuffer builder = null;
			do {
			    /* the index of the first character not yet appended to the builder. */
			    int start = pos;
			    while (pos < limit) {
			        int c = buffer[pos++];
			        ++at;
			        
			        if (c == quote) {
			            /*if (skipping) {
			                return "skipped!";
			            } else*/ 
			        	if (builder == null) {
			                return new String(buffer, start, pos - start - 1);
			            } else {
			                builder.append(buffer, start, pos - start - 1);
			                return builder.toString();
			            }

			        } else if (c == '\\') {
			            if (builder == null) {
			                builder = new StringBuffer();
			            }
			            builder.append(buffer, start, pos - start - 1);
			            builder.append(readEscapeCharacter());
			            start = pos;
			        }
			    }

			    if (builder == null) {
			        builder = new StringBuffer();
			    }
			    builder.append(buffer, start, pos - start);
			} while (fillBuffer(1));
	        throw syntaxError("Unterminated string");

        } catch (IOException ioe) {
        	throw new JSONParsingException(ioe);
		}
    }

    /**
     * Unescapes the character identified by the character or characters that
     * immediately follow a backslash. The backslash '\' should have already
     * been read. This supports both unicode escapes "u000A" and two-character
     * escapes "\n".
     *
     * @throws NumberFormatException if any unicode escape sequences are
     *     malformed.
     */
    private char readEscapeCharacter() throws IOException {
        if (pos == limit && !fillBuffer(1)) {
            throw ioError("Unterminated escape sequence");
        }
        char escaped = buffer[pos++];
        ++at;
        
        switch (escaped) {
        case 'u':
            if (pos + 4 > limit && !fillBuffer(4)) {
                throw ioError("Unterminated escape sequence");
            }
            String hex = new String(buffer, pos, 4);
            pos += 4;
            return (char) Integer.parseInt(hex, 16);
        case 't':
            return '\t';
        case 'b':
            return '\b';
        case 'n':
            return '\n';
        case 'r':
            return '\r';
        case 'f':
            return '\f';
        case '\'':
        case '"':
        case '\\':
        default:
            return escaped;
        }
    }

    /**
     * Get the next value. The value can be a Boolean, Double, Integer,
     * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
     * @param ignoreNext 
     * @throws JSONParsingException If syntax error.
     *
     * @return An object.
     */
    public Object nextValue() throws JSONParsingException {
        char c = nextClean();
        String s;

        switch (c) {
            case '"':
            case '\'':
                return nextString(c);
        }

        /*
         * Handle unquoted text. This could be the values true, false, or
         * null, or it can be a number. An implementation (such as this one)
         * is allowed to also accept non-standard forms.
         *
         * Accumulate characters until we reach the end of the text or a
         * formatting character.
         */
        StringBuffer sb = new StringBuffer();
        char b = c;
        while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) {
            sb.append(c);
            c = next();
        }
        if (c != 0) back();

        /*
         * If it is true, false, or null, return the proper value.
         */
        s = sb.toString().trim();
        if (s.equals("")) {
            throw syntaxError("Missing value.");
        }
        if (s.equalsIgnoreCase("true")) {
            return Boolean.TRUE;
        }
        if (s.equalsIgnoreCase("false")) {
            return Boolean.FALSE;
        }
        if (s.equalsIgnoreCase("null")) {
            return null; // TODO Create a NULL Constant
        }

        /*
         * If it might be a number, try converting it. We support the 0- and 0x-
         * conventions. If a number cannot be produced, then the value will just
         * be a string. Note that the 0-, 0x-, plus, and implied string
         * conventions are non-standard. A JSON parser is free to accept
         * non-JSON forms as long as it accepts all correct JSON forms.
         */
        if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') {
            if (b == '0') {
                if (s.length() > 2 && (s.charAt(1) == 'x' || s.charAt(1) == 'X')) {
                    try {
                        return new Integer(Integer.parseInt(s.substring(2), 16));
                    } catch (Exception e) { 
                        /* Ignore the error */
                    }
                } else {
                    try {
                        return new Integer(Integer.parseInt(s, 8));
                    } catch (Exception e) {
                        /* Ignore the error */
                    }
                }
            }
            try {
                return Integer.valueOf(s);
            } catch (Exception e) {
                try {
                    return new Long(Long.parseLong(s));
                } catch (Exception f) {
                    try {
                        return Double.valueOf(s);
                    }  catch (Exception g) {
                        return s;
                    }
                }
            }
        }
        return s;
    }
    

    public JSONParsingException syntaxError(String message) {
    	back();
        return new JSONParsingException(message + toString());
	}
	
	private IOException ioError(String message) {
    	back();
        return new IOException(message + toString());
	}

	public String toString() {
		int snippetLen = Math.min(SNIPPET_LEN, BUFFER_LEN);
		String sb = " at character " + (at+1) + " >>";
		try {
			if (pos + SNIPPET_LEN < limit || fillBuffer(snippetLen))
				return sb +  new String(buffer, pos, snippetLen) + "...<<"; 
			else 
				return sb + new String(buffer, pos, limit-pos) + "<<";
		} catch (Exception e) {
			return " at character " + (at+1);
		}
    }
	
    /**
     * Returns true once {@code limit - pos >= minimum}. If the data is
     * exhausted before that many characters are available, this returns
     * false.
     */
    private boolean fillBuffer(int minimum) throws IOException {
    	boolean b;
    	pos = (b = pos > 0) ? pos - 1: 0;
        if (limit != pos) {
            limit -= pos;
            System.arraycopy(buffer, pos, buffer, 0, limit);
        } else {
            limit = 0;
        }
        pos = (b) ? 1: 0;
        int total;
        while ((total = reader.read(buffer, limit, buffer.length - limit)) != -1) {
            limit += total;
            if (limit >= minimum) {
//                System.err.println("buffer filled: " + limit + " \"" + new String(buffer, 0, limit) + "\"");
                return true;
            }
        }
        return false;
    }

	public void reset() throws JSONParsingException {
		try {
			reader.reset();
			pos = limit = at = 0;
		} catch (IOException e) {
			throw new JSONParsingException(e);
		}
	}

	public char peek() throws JSONParsingException {
		try {
			if (more() || fillBuffer(1)) {
				return buffer[pos+1];
			}
			return 0;
		} catch (IOException e) {
			throw new JSONParsingException(e);
		}
	}

	
}
